מיון ערימה (Heapsort) מבני נתונים חלק I מיון מבני נתונים ד"ר ערן לונדון. הגדרת ערימה ערימה (בינארית) הינה מערך אשר ניתן להציגו כמו עץ בינארי מלא או כמעט מלא כאשר כל קודקוד בעץ מתאים לתא במערך. העץ הינו מלא בכל הרמות מלבד הרמה (השורה) האחרונה (במקרים שבהם העץ הינו כמעט מלא) שיכולה להיות לא שלמה אבל היא תהיה מלאה מהקצה השמאלי עד לקודקוד כלשהו באותה רמה (שורה) נסמן: [A] length גודל המערך. [A] heap size מספר האלמנטים בערימה. כמובן ש [A] heap size [A] length (מכיוון שלא תמיד גודל הערימה יהיה זהה לגודל המערך). תזכורת לגבי עצים: l עץ בינארי הדרגה של כל קודקוד היא לכל היותר. l עץ בינארי שלם כל העלים באותו העומק ולכל הקודקודים דרגה. l עץ בינארי מלא כל קודקוד הוא עלה או שדרגתו בדיוק. ראוי קודם כל להגדיר את הבעיה ואז להציע דרכים לפתרונה... 0. הגדרה של המושג מיון מיון הוא אלגוריתם אשר הקלט שלו הוא מערך A בגודל :n A = [a, a,..., a n ] ואילו הפלט הינו פרמוטציה של המערך, בסדר עולה או יורד, דהיינו: A = [ a σ(), a σ(),..., a σ(n) ] כאשר: σ(n).a σ() a σ() a הגדרה 0. אלגוריתם מיון מכונה ממין במקום in) Sorts (place אם מספר התאים הנוספים אינו תלוי ב n, כלומר, מספר התאים הנוסיפים הנדרשים הוא קבוע () O. פרמוטציה=תמורה. כלומר סדר כלשהו של האיברים במערך. ניתן לקרוא עוד על פרמוטציות בסיכום של מבנים אלגבריים בסעיף.5 (עמוד 5, מה שנקרא שם: S) X ובפרק 8 (עמוד 8). שורש העץ הינו [] A, וככה בעיקרון בנוי העץ: i a i i+ b c האבא תמיד יהיה גדול מהילדים שלו:.a b a c במילים אחרות לכל קודקוד שהוא אינו השורש ( i):.a [P arent (i)] A [i] מסקנה: הערך הכי גבוה בערימה נמצא בשורש העץ! כדאי לזכור (כמתואר בתרשים): P arent (i) = i Left (i) = i Right (i) = i +.. הגדרת גובה של צומת (קודקוד) בעץ הגובה של קודקוד בעץ מוגדר ע"י המסילה כלפי מטה שצריך לעבור בעץ מאותו קודקוד עד לעלה הכי רחוק כמובן שזה תלוי על איזה מערך מדובר. לא כל מערך הוא ערימה (זה תלוי במיקומי הערכים שבו).
8 5 (כלומר עלה ברמה הכי נמוכה, יכולות להיות כמובן כמה מסילות או דרכים כאלה), ע"י מסילה פשוטה (ללא מעגלים). למשל (ניקח עץ בינארי שאינו ערימה): 7 0 5 הגובה של קודקוד הינו. הגובה של קודקוד הינו 0. הגובה של 0 הינו. לכן, הגובה של העץ הינו הגובה של השורש, במקרה שלנו זה הגובה של קודקוד. היא איברים n עם ערימה של טענה. הגובה.Θ (log n). שימושים שונים בערימה הפעולה זמן הריצה O (log n) Θ (n) O (n log n) O (log n) O (log n) HEAPIFY BUILD-HEAP HEAPSORT EXTRACT-MAX INSERT HEAPIFY.. זהו אלגוריתם אשר מהווה חלק מאלגוריתם מיון הערימה. קלט האלגוריתם: A מערך, i מצביע לאינדקס במערך. חשוב לציין כאן (בשביל לעשות קצת סדר) שאנחנו בעצם עובדים עם מערך, כלומר, ערימה היא בעצם מערך, אבל ישנה דרך הצגה נוחה לאותו מערך והיא עץ בינארי שלם (או כמעט שלם). הנחה לגבי הקלט: אם ניקח קודקוד בעץ, אזי התת עץ שמימינו והתת עץ שמשמאלו, כלומר: (i),left (i), Right הינן ערימות. חשוב לזכור שעדיין יכול להיות שאותו קודקוד [i] A קטן משני הילדים שלו, למשל: ההנחה לגבי הקלט מבטיחה לנו שאם נבחר את הקודקוד שיש בו (הקודקוד עם העיגול הכפול) אזי כל מה שיש מתחתיו, מימינו ומשמאלו, הוא ערימה (ש 8,5 הם השורשים של שתי הערימות (של שני העצים)). יש קודקודים מתחת ל 5,8 אך הם אינם מופיעים בשרטוט. רעיון האלגוריתם: אנחנו בודקים: האם הקודקוד גדול משני הבנים שלו? אם כן אז סיימנו. אם לא מחליפים אותו עם הגדול משני בניו. כעת, אחרי שהחלפנו, אנחנו עושים את אותו הדבר לקודקוד שהחלפנו, כלומר: נניח שבחרנו בקודקוד עם הערך (זה עם העיגול הכפול), אזי אחרי ההחלפה זה יראה כך: 8 5 כעת אנחנו נבצע את אותה בדיוק בדיוק עם הילדים החדשים של ובמידת הצורך נחליף שוב... וכך הלאה, עד שנגיע לעלים... הפלט הוא כמובן שהחל מהמיקום של הקודקוד הנבחר בהתחלה יש לנו ערימה... זמן הריצה של אלגוריתם זה עבור ערימה בגודל n הוא.O (log n).. בניית ערימה BUILD-HEAP זמן הריצה של בניית ערימה ממערך נתון הינו (n) O. (התחלת הרעיון בקצרה: אין לנו צורך לבדוק את העלים, לכן כאשר אנחנו נבנה ערימה ממערך אנחנו נתחיל length(a). מהקודקודים שאינם עלים שזה: length(a) (וככה כלומר, נתחיל מהקודקוד שמספרו אנחנו מדלגים על כל העלים, כי הוכחנו בעבר שבעץ בינארי length(a) קודקודים שאינם עלים, ולכן הקודקוד יש שמספרו הסידורי הוא אזי הוא בוודאי אינו length(a) עלה וכך אלה שמספרם הסידורי קטן ממנו, כל השאר הם עלים) ונרד עד לקודקוד מספר, על כל אחד מהקודקודים הכוונה כמובן היא לערך הקודקוד או התא במערך.
מבני נתונים נעשה.HEAP IF Y בהמשך הדרך גם יש אינדוקציה...). כעת נעשה HEAP IF Y לראש העץ ונקבל: 5 8 8 למשל: כעת, בגלל שיש לנו רק שני קודקודים האלגוריתם הסתיים (שמים את השורש לפני ה 8 ואז את ה וקיבלנו.(, 5, 8 5 במקרה הזה היינו מתחילים מ 5 כי הקודקוד ש"מספרו הסידורי",, הוא הקודקוד עם המספר הסידורי הכי גבוה שאינו עלה... לאחר מכן אנחנו ממשיכים לקודקוד שערכו הוא (כי מספרו הסידורי הוא ).. אלגוריתם מיון הערימה HEAPSORT אחרי שהגדרנו את שתי הפעולות הפעולות הנ"ל (ב. ) אנחנו יכולים לבנות בעזרתן אלגוריתם למיון באמצעות ערימה. אלגוריתם שיקבל מערך שהוא כבר ערימה ויעבוד באופן הבא: כל פעם הוא יוציא את שורש העץ וישים אותן בתא "בצד" (כי זה יהיה התא עם הערך הגדול ביותר), וישים את [i] A העלה עם הערך הכי קטן במקום השורש. אז עושים (,A) HEAP IF Y (לשורש) ושוב פעם מוציאים, אחרי ה HEAP IF Y את מה שיש בשורש. למשל: במקרה הזה מה שיהיה לנו הוא שנוציא את 8 ובמקומו נשים את ולכן, מה שנקבל הוא: מיון מהיר מיון מהיר הוא אלגוריתם רקורסיבי שעובד בשיטת ההפרד ומשול, כלומר, אנחנו מחלקים את המערך לשני חלקים באופן רקורסיבי ו"שולטים" בכל אחד מהם כלומר, ממינים כל אחד מהחלקים ל כמו את הגדול (ולכן גם אותו מחלקים ל ). וכאשר אנחנו משלבים את תתי המערכים אנחנו מקבלים אותם שהם כבר ממוינים. וכך בעצם גם המערך ההתחלתי מתקבל כפלט כמערך ממוין. חשוב לזכור: כשאר אנחנו מפצלים את המערך אסור ששום תת מערך יהיה ריק, כלומר אם יש לנו מערך בגודל n אזי בחלוקה ל צד אחד יהיה בגודל n i והצד השני יהיה בגודל n. i זמן הריצה הממוצע של מיון מהיר הינו (n O n) log ובמקרה הגרוע ביורת זמן הריצה הינו ( O. ( n. כיצד אלגוריתם מיון מהיר עובד? נתון לנו מערך כלשהו, למשל: 6 8 ראישת כל מה שנבחר הינו איבר הפיבוט (הציר), ולשם הפשטות ניקח את 6 שהוא האיבר הראשון להיות הציר (זה יכול להיות גם איבר אחר, אבל בשביל הפשטות נבחר את הראשון), ונתחיל למיין: אנחנו רוצים כי כל מה שיהיה משמאלו יהיה קטן או שווה לו כיצד עושים זאת? אנחנו מתחילים לסרוק את המערך משני הצדדים, כלומר: 6 8 i j כאת אנחנו עושים את הדבר הבא: מקדמים את j (לכיוון i) עד שהאיבר ש j מצביע עליו יהיה קטן או שווה לפיבוט, 8 5 8 5
מבני נתונים ואת i (לכיוון j) עד שהאיבר ש iיצביע עליו יהיה גדול או שווה לפיבוט. כעת, אם i < j נחליף בין התאים. אחרת נחלק את המערך לשני חלקים ונעשוה שוב מיון מהיר לכל אחד מהחלקים... (ככה נמשיך עד שנגיע למצב שיש תאים בודדים). למשל, במקרה שלנו: על ההתחלה התנאי הזה מתקיים ולכן נחליף, בןי האיברים: 8 6 i j ונקדם את j ב, וכמו כן נקדם את i עד שנמצא איבר שהוא גדול או שווה לפיבוט (ל 6 ): מיון מיזוג גם מיון מיזוג הינו אלגוריתם שזמן הריצה שלו הינו.O (n log n) ישנן מספר גירסאות לאלגוריתם זה, אבל הרעיון הכללי הינו כזה: אנחנו כל פעם מחלקים את המערך ל (גם זה אלגוריתם הפרד ומשול) עד שכבר לא ניתן לחלק, ואז ממזגים את המערכים, למשל: נחלק ל : וכעת שוב פעם נחלק ל : 6 8 6 8 8 6 i j ונחליף בניהם: ושוב פעם: 6 8 כעת נתחיל למזג: בהתחלה ניקח כל ונשים את הגדול מימין: 8 6 i j נשים לב שבאיזשהו שלב נגיע למצב ש 6 8 8 6 j i (כלומר.(j i במקרה זה נחלק את המערך ל : כעת נעשה את הדבר הבא: נמזג את שתי הזוגות הראשונים (ישנן כל מיני דרכים למזג, אבל מה שחשוב הוא שלבסוף נקבל מערך ממוין), ניתן לעשות זאת ע"י מיון הכנסה. 5 ולאחר מכן נקבל: 6 8 8 6 ונעשה מיון מהיר לכל אחד מהמערכים וכך הלאה... החלוקה כמובן, היא וירטואלית, כלומר מדובר עדיין באותו מערך ולא בשני מערכים שלאחר מכן צריך לחבר אותם. לפעולת המיזוג ישנן גירסאות שונות. 5 אני לא אסביר כאן על המיון עצמו רק אורמ שזה מיון פשוט יחסית שמתאים בין השאר להכנסת איברים למערך ממוין.
5 5 6 5 6 7 8 וכעת נמזג באותה דרך את הזוג האחרון ונקבל את המערך ממוין... 6 8 מיונים בזמן לינארי ישנם מיונים בזמן ריצה (n) O (שזה אומר שזמן הריצה הוא לינארי), אבל... ישנן הנחות מסוימות על הקלט כדי שמיון זה אכן יעבוד. לפני שניגש למיונים בזמן לינארי חשוב שנגדיר את המושג הבא: מיון יציב. מיון יציב הינו מיון שבמקרה ויש שני איברים שווים אזי בסוף המיון הסדר שלהם נשמר. למשל, אם במערך יש לנו בתא מספר 7 ובתא מספר 7, אזי בסוף המיון שני ה 7 ים יהיו צמודים, אבל זה שהיה בתא יהיה לפני זה שהיה בתא.. מיון מנייה ההנחה במיון מניה היא שניתן לחסום את הקלט ע"י קבוע כלומר גודל הקלט הינו n אבל האיבר הכי גדול בקלט הינו.n כפול K כלומר קבוע O (n) = K n כיצד המיון הזה עובד? נניח המערך הנתון לנו הינו המערך A (אותו מערך כמו בדדוגמאות הקודמות) אזי נשתמש בשני מערכי עזר,B C כאשר B יהיה המערך הממוין. ניתן לראות כי ניתן לחסום את אבירי המערך ע"י 8 ולכן זה יהיה הגודל של מערך C. מערך C יכיל את תדירויות אברי A. 6 8 אזי C יהיה (השורה התחתונה מציינת את מספר התא ובמערך עצמו מה התידרות שלו במערך המקורי): 0 0 0 5 6 7 8 כעת מה שנעשה הוא שנסכות את אברי המערך, כלומר נתחיל מהתא הראשון ובתא השני נשים את ערכו של התא הראשון + התא השני, בשלישי נשים את מה שיש בתא השני + הערך שבתא השלישי, וכך הלאה עד התא האחרון. כעת C יראה כך: כעת נתחיל לבנות את מערך הפלט B: נלך אל התא האחרון ב Aונסמן את ערכו ב j. נסתכל על [[j] B C] ולשם נכניס את j. כעת נוריד את ערכו של [j] C ב ונעשה את אותו הדבר עבור אורך המערך פחות וכך הלאה עד שנגיע לתא הראשון. בדוגמא שלנו: בהתחלה = j נפנה למערך C (מדברים על המערך אחרי שעידכנו אותו, זה שנמצא כאן ממש למעלה...) ונסתכל בתא השני נראה שהערך שם שווה ל, כעת נלך ל B ונשים בתא מספר את j (שהוא גם ) ונקטין ב C את הערך ב. כעת מערך C יראה כך: ומערך B כבר נראה כך: 5 5 5 6 6 7 5 6 7 8 כעת נסתכל על התא האחד לפני אחרון ב A נראה שערכו הוא ולכן =.j כעת נסתכל על [] C ונראה שהוא, לכן נציב את בתא מספר במערך B: ונעדכן את C בהתאם וכך הלאה... לבסוף נחזיר את B כפלט ונקבל את המערך ממוין.. מיון בסיס Sort) (Radix גם הוא מיון יציב שמניח על הקלט שניתן לחסום אותו ב ( n ) O. 5
מה שאנחנו עושים הוא שאנחנו דואגים שלכל המספרים יהיה את אותו אורך (אפשר גם להוסיף אפסים לפני במידת הצורך), ואז מתחילים מהספרה האחרונה (ספרת האחדות) ממינים לפיה באופן הבא: מחלקים את המספרים שבקלט לערימות לפי הספרה האחרונה ואז מסדרים כל מספר אחד על השני לפי הספרה האחרונה, כלומר: נניח והקלט שלנו הינו: חלק II עצים 00 7 6 078 אזי זה כבר מחולק לנו לפי הספרה האחרונה (בכל עומדה יש ספרה אחרונה אחרת, במידה והיו מספרים עם אותה ספרת אחדות, אזי היינו שמים אותם אחד מתחת לשני), כעת נערום אותם אחד על השני לפי ספרת האחדות: 00 6 7 078 כעת נחלק את זה לקבוצות לפי ספרת העשרות ונשים אחד על השני שוב (ונקבל שוב את מה שיש למעלה לכן לא עשיתי את השלב הזה אלא רק תיארתי אותו). ולבסוף נעשתה את זה לפי ספרת האחדות, כלומר נפזר את המספרים ע"פ ספרת האחדות: 00 078 7 6 וכאשר נערום את זה אחד על השני נקבל את המספרים ממוינים. (ניתן לראות כי כבר הם ממוינים). 5 עצי חיפוש בינאריים תזכרות לגבי סימון בעצים: = n מספר הקודקודים. = m מספר הצלעות. 5. הגדרה ומושגים בסיסיים עץ חיפוש בינארי הוא עץ בינארי שללכל קודקוד בו יש בן שמאלי ובן ימני והורה. אם ילד אחד או הורה חסרים, אנחנו ממלאים את החסר ב NIL. נניח כי לנו עץ בינארי T אשר שורשו r. ניקח את אחד הקודקודים בעץ x: אזי כל קודקוד y במסילה r x הינו אב קדמון של x. כדאי לזכור: אם y הוא אב קדמון של x, אזי x הוא צאצא של y. תת העץ של x הינו כל הצאצים של x, או במילים אחרות כל הקודקודים ש x הוא שורשם. נזכיר שני מושגים בסיסיים: קודקוד חיצוני = עלה. קודקוד פנימי = מה שלא עלה. 6
8 הוא האבא של ו. ו הם הילדים של, וכמו כן ו הם אחים. השורש הינו הקודקוד היחיד בעץ שיאן לו הורה. מספר הילדים (לא צאצאים!) של קודקוד x נקראת הדרגה של x. אם r הוא שורש העץ אזי אורך המסילה r x נקרא העומק של x. והעומק הכי גדול של העץ נקרא (או מכונה) גובהו של העץ. למשל: 6 8 הדרגה של 8 היא, הדרגה של 6 היא. העומק של 8 הוא והעומק של 6 הוא. גובה העץ = לעומק של = ( הוא שורש העץ). הערך שנמצא בכל קודקוד יסומן ע"י [x].key במקרה שלנו קודקוד x הוא ה 8. 5. טיולים בעצי חיפוש בינאריים 6 ישנן שלושה דרכים לטייל בעץ חיפוש ביארי,Inorder ו Postorder Preorder בכל שלושת הטיולים בעץ אנחנו תמיד פועלים באופן הבא: הקלט הינו קודקוד כלשהו (בדרך כלל זה יהיה שורש העץ, כי אנחנו נרצה להדפיס את העץ כולו) אנחנו קודם הולכים לתת העץ השמאלי ואז לתת העץ הימני, השאלה היא מתי אנחנו מדפיסים את ערך העלה. כמובן שכאשר אנחנו מסימיים מתי שאין יותר לאן להמשיך (כי הגענו לעלה ואז אין לו תת עץ ימני ושמאלי). הדוגמאות של ההדפסה בטבלה הבאה יתחייסו לעץ הבא: 8 5 7 5. עץ בינארי זהו מבנה של קבוצה סופית של קודקודים, כאשר או שאין לו קודקודים בכלל או שהוא מורכב משלוש קבוצות זרות : 6 ˆ קדוקוד אחד שהוא עלה. ˆ עץ בינארי שנקרא תת העץ השמאלי. ˆ עץ בינארי שנקרא תת העץ הימני. עץ בינארי ללא קודקודים נקרא העץ הריק. 5. עץ חיפוש בינארי בעץ חיפש בינארי מתקיים הכלל הבא: עבור קודקוד x: אם קודקוד y נמצא מימינו של x אזי [y] Key [x],key ואם קודקוד y נמצא משמאלו של x אזי מתקיים [x],key [y] Key למשל: 6 אסור לקודקוד להיות ביותר מקבוצה אחת. סוג הטיול Inorder Preorder Postorder מתי מדפיסים את ערך העלה בדוגמא אחרי שנכנסים לתת,,5,6,7,8, העץ השמאלי ברגע שמגיעים 6,,,5,7,8, לקודקוד (לפני שנכנסים לתת העץ השמאלי) אחרי שנכנסים לתת,5,,7,,8,6 העץ הימני חשוב לזכור שכאשר מגיעים לעלה אז תמיד מדפיסים את ערכו לא משנה באיזה סוד של טיול אנחנו... זמן הריצה של הטיולים בעץ הינו שווה והוא (n) O 7
5.5 חיפוש איבר מינמלי ומקסימלי בעץ 5.7. עוקב בשביל למצוא איבר מינמלי בעץ אנחנו מתחילים מהשורש (או מקודקוד כשלהו אם רוצים למצוא את המינמלי בתת עץ). בשביל מציאת האיבר המינימלי אנחנו כל הזמן הולכים שמאלה עד שהשמאלי הבא הוא.NIL לגבי האיבר המקסימלי אותו דבר רק ימינה. זמן הריצה של כל אחת מפעולות אלו: (n O. (log 5.6 הוספה של איבר לעץ ההוספה היא יחסית פשוטה אנחנו מוסיפים עלים בלבד. כאשר אנחנו רוצים להוסיף אנחנו מתחילים משורש העץ אם הערך שאנחנו רוצים להוסיף גדול או שווה לו אנחנו הולכים לבן השמאלי, אחרת הולכים לבן הימני. אם יש שם קודקוד אנחנו מתחילים את התהליך מההתחלה רק שבמקום השורש אנחנו עושים את זה לקדוקוד שבו אנו נמצאים כרגע. אם אין קודקוד אנחנו מוסיפים את הערך שלנו כעלה. וסיימנו! זמן הריצה של הוספת איבר לעץ הינו: (n O (log בעץ חיפוש בינארי העוקב של קודקוד מסוים הינו הקודקוד בעל הערך הכי קטן מבין כל הגדולים ממנו. בשביל למצוא את העוקב אנחנו מוצאים את האיבר המינימלי של תת העץ השמאלי 7 של אותו קודקוד ואז מתחילים ללכת ימינה עד שנגיע לעלה. 5.7. קודם אותו דבר בדיוק רק הפוך מציאת הקודם של קודקוד x הינה כך: אנחנו מחפשים את האיבר הכי גדול מבין כל הקטנים, לכן: נחפש את האיבר המקסימלי של תת העץ הימני של x ואז נלך כל הזמן שמאלה עד שנגיע לעלה. הערות: זמן הריצה של מציאת הקודם הוא כמו זמן הריצה של מציאת העוקב: n).o (log 5.7 השמטה של איבר מהעץ ישנן שתי אפרויות או שאנחנו אוצים להשמיט עלה או שאנחנו רוצים להשמיט קודקוד שאינו עלה. אם הקודקוד היני עלה אזי אין בעיה פשוט משמיטים את הקודקוד. למשל, עם מהעץ הבא נרצה להשמיט את אזי: 8 8 אם הקודקוד אינו עלה, אזי אם יש לו בן אחד אזי פשוט מוחקים אותו במקומו שמים את הבן היחיד. אם הקודקוד אינו עלה ויש לו שני בנים אזי אנחנו צריכים למצוא את העוקב שלו ובמקום אותו קודקוד לשים את העוקב... זמן הריצה של השמטה הינו: (n O. (log 7 כלומר, אנחנו מתחילים לחפש את האיבר המינימלי כאשר הבן השמאלי של אותו קודקוד יהיה שורש העץ. 8
6 עצים אדומים שחורים 6. בניית עץ אדום שחור בעיקרון, ניתן לבנות עץ אדום שחור חוקי ע"י הוספת קודקוד קודקוד (איך מוסיפים קודקודים יוסבר בהמשך), אבל ישנן עוד כמה שיטות... אציג כאן שתי שיטות מאוד דומות: האחת בונים עץ חיפוש בינארי מאוזן (עד כמה שניתן) ופשוט צובעים את כל העץ בשחור מלבד הרמה שאינה מואזנת (זאת ש"פוגעת" באיזון העץ), למשל: 5 6 7 8 6. הגדרה עץ אדום שחור הינו עץ חיפוש בינארי אשר לכל קודקוד ישנה תוכנה נוספת הוא צבוע באדום או שחור. 8 כלומר, לכל קודקוד ישנן ארבעה תכונות: ˆ הורה (מלבד השורש כמובן). ˆ בן שמאלי. ˆ בן ימני. ˆ צבע (אדום או שחור). 6.. תכונות העץ לעץ בינארי אדום שחור קיימות מספר תכונות: ˆ השורש הוא תמיד שחור. ˆ אין שני קודקודים אדומים ברצף (כלומר, לא ייתכנו אב ובן אדומים). ˆ אנחנו "מרפדים" את תחתית העץ ב NIL ים (שכאן יסומנו כ ). למשל: 6 ˆ הגובה השחור של כלקודקוד מוגדר היטב, כלומר, אם ניקח קודקוד מסויים בעץ, אזי כמות העלים השחורים שנראה (לא כולל אותו במידה והוא שחור) בכל מסילה פשוטה למטה לאחד העלים תהיה זהה. 8 במהלך הסיכום הזה צמתים אדומים יסומנו וצמתים שחורים יסומנו זה עץ אדום שחור חוקי: ˆ השורש שחור. ˆ אין שני אדומים ברצף. ˆ כל קודקוד שנבחר בעץ, הגובה השחור שלו זהה לכל בכל מסילה פשוטה (כלפי מטה) לאחד העלים. דרך נוספת היא שפשוט נצבע רמה אחת בשחור (השורש) ואז רמה הבאה באדום וכך הלאה... ישנם מקרים שבהם זה לא יעבוד, מבחינת הגובה השחור של העלים (זה קורה כאשר הרמה שאינה מאוזנת, כלומר, זאת שמפרה את איזון העץ צבועה בשחור ומה שמעליה באדום), לכן תמיד כדאי לבדוק את הגובה השחור של הקודקודים. 5 6 7 8 הערה: מכאן ואליך לא יוצגו בעצים ה, בהנחה כי הרעיון הובן. כאשר אנחנו נדרשים לצבוע עץ קיים כדי לבדוק אם ניתן לצוע אותו כך שיהיה אדום שחור, כדאי לשים לב למקומות הלא מאוזנים בעץ (היכן שיש תתי עץ שאינם מאוזנים ולאות אם הדבר אפשרי שמה). כלומר, שכל תתי העצים של כל קודקוד בם באותו עומק.
6. הוספת קודקוד לעץ כאשר אנחנו רוצים להוסיף קודקוד לעץ הוא תמיד יתווסף כעלה בצבע אדום. אנחנו מסירים את שכבת ה NIL ים (ה ) באופן זמני, מכניסים את העלה האדום, ואז מחזירים את ה NIL ים. למשל, בעץ כאן שלמעלה, ניתן לראות אותו כאילו הוספנו לנו עכשיו את... נחלק את ההוספה לשלושה מקרים: 6.. הוספת עלה לקודקוד שחור במקרה כזה אין לנו שום בעיה! כי: ˆ זה לא יוצר רצף של שני אדומים (הרי הוספנו אותו מתחת לקודקוד שחור). ˆ זה לא פוגע בגובה האדום של ההורים (והאבות היותר עליונים) כי אנחנו לו סופרים קודקודים שחורים בגובה שחור. 6.. הוספת עלה לקודקוד אדום במקרה הזה יש לנו בעיה יש רצף של שני אדומים... אז מה עושים? לפני זה כדאי שנכיר שני מושגים רוטציה לימין ורוטציה לשמאל רוטציה לשמאל בעיקרון כאן אנחנו מוסבבים קודקוד אחד ימינה (במידה וניתן), למשל: γ β α γ α β כדאי לשים לב לכך ש: ˆ מה שקורה בפועל זה שאנחנו רק מעלים קודקוד אחד (או מורידים, אבל בכל מקרה אנחנו משנים את מיקומי שני הקודקודים) אבל הסדר נשאר רק הקודקוד (או תת העץ) האמצעי (β) זז לצומת השנייה! חשוב לזכור את זה...,α,β γ אלו יכולים להיות קודקודים או תתי עצים. רוטציה לימין בדיוק אותו הדבר רק הפוך: כעת ניתן לראות מה קורה שמוסיפים עלה לקודקוד אדום. ישנם ארבעה מקרים במקרה כזה מה שעושים הוא שמסתכלים על דודו של הקודקוד. מקרה ראשון הדוד אדום 7 6 8 במקרה הזה נניח כי הכנסנו כעלה האחרון אזי דודו הוא 6 (אחיו של אביו) אנחנו רואים כי הוא אדום כלומר יש לנו מקרה של שני בנים אדומים עם אב שחור. במקרה הזה, הסבא 7, מוותר על צבעו והופך לאדום ואילו שני בניו הופכים לשחורים 7 6 8 וע"י כך נפתרה הבעיה. הערה: רק במקרה ש 7 הוא השורש (או במילים אחרות רק במקרה של השורש) אנחנו משאירים אותו שחור ומקבלים את בניו שחורים, לכן במקרה כזה התוצאה תהיה כזאת: 7 6 8 מקרה שני הדוד שחור והכנסנו בן מהכיוון של האב (בן ימני לאב ימני או בן שמאלי לאב שמאלי) 7 6 8 7 עצי B (B-Trees) (מתוך תרגיל בית שקיבלנו) γ α β γ β α 0
7. הגדרה עץ B-tree הוא עץ T ששורשו הוא: ] T] root ומאופיין ע"י:. כל קודקוד x מכיל את השדות הבאים: [x]. n מספר המפתחות אשר נמצאות בקודקוד.(x). המפתחות עצמם, אשר נמצאים בסדר שאינו יורד, כלומר: key [x] key [x] key [x] key n[x] [x]. משתנה (או "ערך") בולאני אשר אומר לנו אם x הוא עלה. T rue אם כן, F alse אחרת.. כל קודקוד פנימי x מכיל + [x] n פוינטרים [x] c [x],..., c n[x]+ לילדים שלו. היות ולעלה אין ילדים אזי ה c i שלהם אינו מוגדר.. המפתחות [x] key i מפרידים בין המפתחות שנמצאים בכל תת עץ, כלומר: אם k i הוא מפתח כלשהו, אשר נמצא בתת העץ ששורשו הינו [x] c i אזי: k key [x] k key [x] k n[x]+ key n[x]+ [x]. כל העלים באותו הגובה (h h הוא גם גובה העץ). 5. ישנם חסמים עליונים ותחתונים על מספר המפתחות שקודקוד יכול להכיל. חסמים אלו ניתנים לביטוי במונחים של t אשר נקרא המעלה המינימלית של העץ כאשר t N וכאשר: 5. כל הקודקודים הפנימיים מלבד השורש מכילים לפחות: t מפתחות ולפחות t ילדים. אם העץ אינו ריק, אזי השורש חייב להכיל לפחות מפתח אחד. 5. כל קודקוד מכיל לכל היותר t מפתחות. לכן, קודקוד פנימי יכול להכיל לכל היותר t ילדים. אם ישנם בדיוק t ילדים אנו אומרים כי הקודקוד מלא 7. חיפוש בעץ אלגוריתם החיפוש מאוד דומה לחיפש בעץ בינארי. במקום לרדת כל פעם ימינה או שמאלה אנחנו עושים זאת בהתאם למספר הילדים שיש לאותו קודקוד. כלומר, מדובר בהכללה של חיפוש בעץ חיפוש בינארי. כלומר, עבור קודקוד x נבצע + (x) n החלטות (סעיפי החלטה) (במקום בעץ חיפוש בינארי). הקלט של האלגוריתם הוא פוינטר x לשורש העץ ומפתח k שאותו מחפשים. הפלט הוא NIL אם k אינו נמצא בעץ, אחרת הפלט הינו הזוג הסדור (i,y) כאשר y הוא הקודקוד ו i הוא אינדקס כך ש: key i [y] = k תיאור האלגוריתם: נחפש את ה i הקטן ביותר המקיים (k) k > key i וגם.i n (x) נבדוק האם ה i הנ"ל מקיים [x] k: = key i ˆ אם כן נחזיר את (i,x). ˆ אחרת נבדוק האם x הוא עלה: אם כן נחזיר.NIL אם לא נקרא שוב לפונקציה באופן רקורסיבי כאשר נעביר לה את k).(c i [x], זמן הריצה: אנחנו קוראים לפונקציה לכל היותר h פעמים כגובה העץ. כאשר במקרה הכי גרוע נבצע בכל קודקוד t השוואות ולכן זה יהיה סה"כ: O (h t) = O (t log t n). לכל מפתח n ב B-tree עם n מפתחות, גובה h ומעלה המינימלית היא :t h log t [ n + ]
מבני נתונים חלק III צפני הופמן 8 חישוב צופן הופמן כיצד מחשבים צופן הופן? נניח ונתונה לנו טבלת התדירויות של כל תו במשפט, אזי מה שאנחנו עושים הוא שלוקחים את שני התווים בעלי התדירות הכי נמוכה וממזגים אותם. למשל אם נתון לנו שהאות a מופיעה פמעים והאות 5 b אזי המיזוג יראה כך: a : b : 5 ומתייחסים ל כאל תו בפני עצמו (כי אנחנו ככל הנראה נרצה להמשיך ולמזג אם במידה וישנם עוד תווים). לבסוף, אחרי שקיבלנו את העץ, אנחנו מוסיפים לכל צלע שמאלית (כלומר, צלע לבן שמאלי) את הערך 0 ולכל צלע ימנית את הערך. לחישוב ערך של תו מסוים אנחנו מתחילים משורש העץ עד לעלה של אותו תו (כל התווים הם בעלים של העץ כמובן) וכל פעם אנחנו מלקטים את הספרה שדרכה אנחנו עוברים (בהתאם לצלע) וזה צופן של התו הספציפי. חשוב לזכור שני דברים:. בשורש העץ אמור להיות לנו סך המופעים הכולל של כל התווים.. אם מדובר בצופן בעברית אזי בסוף שאנחנו כותבים לכל אות את הקוד שלה, אנחנו צריכים לעשות את זה הפוך. למשל, אם נדרשו להצפין את המשפט: הכלב הלך. אזי לבסוף שנכתוב את הצופן עבור כל אות, בשביל זה יהיה נכון נצטרך לכתוב את זה כך: ךלה בלכה. חלק IV אלגוריתמים בתורת הגרפים מטריצת השכוניות הינה מטריצה ריבועית בגודל m m אשר התא (j,i) פירושו ש i שכן של j. ניקח למשל את הגרף המכוון הבא: אזי מטריצת השכנות שלו תהיה: 0 0 0 0 0 0 0 0 0 0 0 0 כמובן שאם צלע היא שכנה של עצמה (אנחנו מרשים צלע מקודקוד לעצמו) אזי באלכסון יהיה ולא 0. אם הגרך הנ"ל היה גרף שאינו מכוון אזי מטריצת השכנות היתה: 0 0 0 0 0 0 0 0 כשמדובר בגרף שאינו מכוון המטריצה היא תמיד סימטרית. רשימה שכנים הינה רשימה של רשימות מקושרות שבכל תא יש קודקוד ואחריו את רשימה השכנים שלו. אנחנו נראה דוגמר של גרף מכוון (אותו גרף שנתון למעלה): חקירת גרף BFS DFS כאשר יש לנו גרף מכוון או לא מכוון ניתן להציג את אותו במצעות שתי אפשרויות: מטריצת שכנויות או רשימת שכנים.
כעת, אם היה מדובר בגרף הלא מכוון, או בגרפים שיש לכל קודקוד יותר משכן אחד, אזי ניתן לשים את הקודקודים ברשימה באופן שרירותי, אבל זה כמובן ישפיע על סדר הכניסה לקודקודים באלגוריתמים BFS ו DFS. למשל, במקרה שלנו, אם היה מדובר בגרף שאינו מכוון:. חקירה רוחבית של הגרף BFS באלגוריתם הזה אנחנו נחקור את הגרף לרוחב. הקלט הינו גרף G וקדוקוד מקור.(s V ).s כמו כן, ישנו תור שאופן השימוש בו יוסבר במהלך תיאור האלגוריתם. פלט האלגוריתם הינו עץ מרחקים מינמליים 0 כאשר לכל קודקוד ישנם את המאפיינים הבאים: [u] d המרחק של של כל קודקוד u מקודקוד s (המרחק המינמלי). [u] π הקדוקוד שגילה את קודקוד u, "האבא שלו". (מלבד.(NIL הוא שלו שה π s האלגוריתם לכל קודקוד יכול להיות צבוע באחד משלושת הצבעים הבאים: לבן (צבע ברירת המחדל שלו), אפור ושחור. נתחיל מהכלל הבא: כל קודקוד שנכנס לתור נצבע באפור וכאשר הוא יוצא ממנו הוא נצבע בשחור. האלגוריתם יסתיים כאשר כל הקודקוד יהיו שחורים (והתור יהיה ריק). אנחנו מכניסים קודם כל את קודקוד s לתור וצובעים אותו באפור, אנחנו מוציאים אותו מהתור, צובעים אותו בשחור ואת כל השכנים שלו אנחנו צובעים באפור (ומכניסים אותם לתור) ומסמנים את ה π שלהם ב s ואת ה d שלהם ב. אנחנו שומרים על הכלל הבא: אנחנו נוגעים רק בקודקודים לבנים. אם קודקוד הוא שחור או אפור אנחנו לא נוגעים בו (כלומר, לא מכניסים אותו לתור). כעת, אנחנו מוציאים את הקודקוד הראשון שנמצא בתור מסמנים את כל שכניו הלבנים באפור ואותו בשחור. אנחנו מוציאים את הקודקוד השני בתור מסמנים את כל שכניו הלבנים באפור ואותו בשחור. וכך הלאה, עד שנראה שאין יותר קודקודים לבנים. נוציא את הקודקודים מהתור וקיבלנו עץ מרחקים מינמליים. ביותר. 0 כלומר, עץ שהמרחק של הקודקוד s מכל שאר הקודקודים הינו הקצר. חקירה עומקית של הגרף DFS הקלט ב DFS הוא גרף G עם רשימת השכנים. הפלט הינו גרף חסר מעגלים כאשר לכל קודוק דישנו זמן כניסה אליו וזמן יציאה. גם אם ישנו יותר מרכיב קשירות אחד כולם יכללו בגרף היות ואנחנו עוברים קודקוד קודקוד (ב BFS, לא נגיע אל קודקודים שאינם באותו רכיב קשירות של s). ישנו שעון שכל תזוזה (כניסה אל קודקוד או יציאה ממנו) מגדילה את המונה שלו ב. איך אלגוריתם DFS פועל? הולכים ע"פ רשימת השכנים: מתחילים מהקודקוד הראשון ברשימה וממשיכים לקודקוד הראשון נמצא אצלו ברשימת השכנים, ואז הולכים לקודקוד הראשון אצלו וכך הלאה עד שמגיעים למצב שלא ניתן להמשיך (כמו ב BFS לא נוגעים בקודקוד שכבר היינו בו. אם מגיעים לקודקוד כזה אז: או שממשיכים הלאה לשכן הבא של אותו קודקוד, ובמידה ואין אזי ממשיכים לקודקוד הבא ברשימת הקודקודים [רשימת השכנות]). כל קודקוד שנכסים אליו מעדכנים את זמן הכניסה אליו וכשיוצאים ממנו מעדכנים את זמן היציאה ממנו. לבסוף נקבל גרף קשיר חסר מעגלים. ניקח למשל את הגרף הבא: אזי נלך מ ל וזמן הכניסה של יהיה ושל יהיה וכנ"ל לגבי. אז נראה שאין לאן להמשיך ואז נתחיל לחזור חזרה הזמן יציאה של יהיה של יהיה 5 ושל יהיה 6. ואז נמשיך הלאה בקודקודים (כי סיימנו את,,) ונגיע לקודקוד שזמן הכניסה שלו יהיה 7 וזמן היציאה שלו יהיה.8 והגרף שנקבל הינו:. מיון טופולוגי מיון טופולוגי ניתן לעשות ב DAG בלבד גרף מכוון ללא מעגלים!
מיון טופולוגי הוא בעצם סידור לינארי המטרה במיון טופולגי היא שנוכל למיין את האיברים בכיוון אחד, כלומר שכל הצלעות יהיו בכיוון אחד ואז יהיה ניתן להציג את כל הקודקודים בשורה (למשל). איך עושים זאת? עושים DFS לגרף ואז מסדרים את הקודקודים לפי זמן היציאה, למשל בדוגמא שלנו: אזי העץ הפורש המינימלי יהיה: ונשים לב כי כל החצים הם באותו הכיוון... 0 עצים פורשים מינמליים 0. עץ פורש מינימלי (הגדרה) 0. האלגוריתם הפשוט לבנית עץ כזה מגרף G לוקחים קבוצת צלעות A (אשר בהתחלה היא הקבוצה הריקה). וכל עוד A אינה מכילה עץ פורש אנחנו מוסיפים לה את צלע (v,u) בתנאי שהיא צלע בטוחה. בשביל להגדיר מהי צלע בטוחה נגדיר קודם חתך: חתך של גרף לא מכוון G הוא חלוקה של הגרף לשתי קבוצות:.S, V \S צלע (v,u) חוצה את החתך אם כל אחד מהקצוות נמצא בקבוצה אחרת. אנחנו אורמים כי החתך מכבד את A אם אין צלע ב A שחוצה אותו. צלע נקראת צלע קלה אם היא חוצה את החתך ויש לה משקל מינימלי (יכולות להיות כמה שחוצות, אבל כולן צריכות להיות בעל משקל מינימלי [המשקל צריך להיות זהה]). אנחנו יודעים כי צלע זאת היא צלע בטוחה. למשל: 7 נניח ויש לנו גרף G עם פונקצית משקל + R w : E (בהמשך יהיה ניתן להניח גם משקל שלילי) על הצלעות שלו. כלומר לכל צלע ישנו משקל, למשל: אזי המשקל של הינו.7 b) a פירושו המסילה מ a ל b ). כעת, עבור גרף נתון, מכל העצים הפורשים נחפש את תת העץ שסכום משקל צלעותיו הוא מינימלי. למשל ניקל את הגרף הבא: ניתן לראות כי החתך (הקו המקווקו) מכבד את החלוקה של (,) ו (,) ועילו כעת נוכל להוסיף את הצלע הבטוחה (,) כי היא צלע קלה (ויוצרת עץ פורש). כמו כן אם (,) כבר הייתה צלע היה ניתן להוריד אותה ולקחת במקומה את (,) לעץ הפורש. 0. אלגוריתם קרוסקל למציאת עץ פורש מינימלי קלט האלגוריתם הינו גרף G ופונקצית משקל w. באלגוריתם של קרוסקל אנחנו גם מתחילים מקבוצה A שהיא ריקה. 6
אנחנו הופכים את כל הקודקודים בעץ לרכיבי קשירות נפרדים (אנחנו באופן וירטואלי משמיטים את כל הצלעות כך נשארים רק הקודקודים). אנחנו ממינים את E (צלעות הגרף) בסדר לא עולה ע"פ המשקל שלהן. כעת אנחנו מתחילים לעבור על הצלעות (מהקטנה לגדולה): אם עבור צלע,u) (v E,u v נמצאים בשני רכיבי קשירות שונים מכניסים את הצלע (v,u) ל A. סיימנו כאשר עברנו על הצלעות וקיבלנו כפלט עץ פורש מינימלי. זמן הריצה של האלגוריתם: ( E O. E ) log 0. אלגוריתם פריים למציאת עץ פורש מינימלי באלגוריתם זה אנחנו נתונים כקלט לא רק גרף ופונקציית משקל, אלא גם קודקוד התחלתי r. אנחנו מכניסים את כל צלעות G לתור עדיפויות Q. כעת נגדיר לכל קודקוד ב G פונקציה.Key נתאחל את ה Key של כל הקודקודים בתור ל מלבד זה של.Key [r] = 0 :r כמו כן:.π [r] = NIL כעת אנחנו מקיימים את הלולאה הבאה עד ש Q יהיה ריק: אנחנו כל פעם מוציאים מ Q את הקודקוד עם הערך המינימלי ובודקים עבור כל שכן שלו v האם: v Q וגם [v] w (u, v) < Key אזי: מעדכנים: π [v] = u וגם v).key [v] = w (u, במילים אחרות: כל פעם שאנחנו מגיעים לקודקוד אנחנו בודקים לגבי כל אחד משכניו אם הוא ב Q וגם משקל הצלע בניהם קטנה מה Key של השכן. אם כן אנחנו מעדכנים שאת v גילה π) [v] = (u u ומעדכנים את ה Key של השכן בהתאם (ב ( v w).,u) זמן הריצה של אלגוריתם זה הינו: ( V O E ) log מסילות קצרות ביותר ומרחקים בחלק הבא, יוצגו הסברים קצרים והדגש יהיה בעיקר על האלגוריתמים השונים (הקלט והפלט שלהם ולא תמיד התיאור שלהם).. הקדמה (מקודקוד מסוים לכל הקודקודים) נתון לנו גרף מכוון עם פונקצית משקל שיכולה להיות גם לא חיוביות. השאלה איך אנחנו מוצאים את המסילה הקצרה ביותר מקודקוד מסוים לכל הקודקודים או מכל קודקוד לכל קודקוד. הערה לגבי מעגלים בגרף: ניתן להניח כין אין מעגלים (או אם ישנן, אז במקרים מסוימים אין טעם להמשיך לחפש מסילות קצרות), כי: א. אם ישנו מעגל חיובי (או שסכומו הינו 0) אזי אין טעם להיכנס אליו. ב. אם ישנו מעגל שלילי אזי נאחנו ניכנס אליו ולא נצא. לכל קודקוד בגרף הזה אנחנו נשים את האורך או המשקל אליו, למשל: 0 כמובן שהערכים בקודקודים יכולים להיות שליליים, יכולים להיות או. בשביל למצוא את המסילות הנ"ל מאותו קודקוד נעשה את הדבר הבא: את קודקוד המקור נאתחל להיות 0 ואת שאר הקודקודים ל. 5
. פעולת ההרפיה Relax פעולת ההרפיה אומרת לנו אם ישנה דרך קלה יותר להגיע אל קודקוד. למשל, נניח ונתון לנו המצב הבא: אזי ניתן לראות שאם נלך דרך הצלע הנתונה אזי המשקל עבור המסילה מ s (קודקוד המקור) לקודקוד הימני תהיה קצרה אזי אנחנו עושים את פעולת ההרפיה ונקבל: 5 ונעדכן את זה שגילה את הקודקוד שיש בו 5 להיות קודקוד. באופן כללי התנאי לעידכון (להרפיה) הוא כזה: עבור שני קודקודים,u: v אם v) d [v] > d [u] + w (u, אזי: d [v] = du + w (u, v) וגם π [v] = u. אלגוריתם בלמן פורד (תיאור בלבד) הקלט של אגלוריתם בלמן פורד הינו: גרף, פונקצית משקל וקדוקוד מקור. האלגוריתם מחזיר אמת אם אין מעגל שלילי בגרף ושקר אם ישנו מעגל שלילי. האלגוריתם עוזר לנו למצוא את המסילה הקלה ביותר לכל קודקוד מקודקוד המקור. זמן ריצת האלגוריתם הינו ( E O. V ) ראשית כל אנחנו מאתחלים את כל הקודקודים כמתואר ב.. ומגדירים קבוצת קודקודים = S. אנחנו מייצרים תור עדיפויות Q שאליו נכניס את הקודקודים וכל פעם שנשלוף ממנו קודקוד זה יהיה המינימלי. אנחנו נוסיף את הקודקוד ל S ומיד אחרי ההוספה נעשה Relax לכל הצלעות היוצאות מאותו קודקוד. זמן הריצה: זמן הריצה יכול להשתנות כתלות באירך שאנחנו שולפים את הקודקודים: אם אנחנו שולפים את הקודקודים ממערך אחד אחרי השני (קודקוד מס' ואז וכך הלאה...) אזי זמן הריצה הינו:.O ( V ) אם אנחנו משתמשים בערימת מינימום (השורש הוא האיבר המינימלי) אזי זמן הריצה הינו: ) V O (( V + E ) log ואם אנחנו משתמשים בערימת פיבונאצ'י אזי זמן הריצה הינו: E ) O ( V log V + (זהו זמן הריצה הטוב ביותר). מסילות קצרות ביותר ומרחקים בין כל זוגות הקודקודים מקודם דיברנו על כך שיש קודקוד מקור ורצינו למצוא את המרחקים הקלים ביותר ממנו לכל קודקוד בגרף מכוון עם פונקצית משקל לכל צלע. כעת אנחנו מדברים באופן כללי אנחנו רוצים למצוא את המסילות הקלות ביותר שישנן בלי קודקוד מקור.... מטריצת המסליות. אלגוריתם בגרף מכוון חסר מעגלים (DAG) הקלט הינו גרף, פונקציית משקל וקודקוד מקור. היות ומדובר ב DAG אזי אנחנו עושים מיון טופולוגי, ולכל קודקוד שאנחנו לקוחים ע"פ המיון הטופולוגי אנחנו עושים Relax לכל השכנים שלו..5 אלגוריתם דייקסטרה (Dijkstra) אלגוריתם זה מאוד דומה לבלמן פורד רק שזמן הריצה שלו טוב יותר. כמו כן, אנחנו גם מניחים באלגוריתם זה כי בגרף אין משקלים שליליים. 6